//
//  SPModelBase.swift
//  
//
//  Created by LSP on 2021/1/9.
//

import Foundation
import SPFoundationCategory

let SPMODEL_SAVED_PATH: String = {
    if var path = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first {
        return path + "/SPModels"
    }
    return "/var"
}()


// MARK: Protocols
public
protocol SPModel {
    static
    func from(dics: [[String: Any]]) -> [SPModel]
}

public
protocol ModelObserverDelegate: NSObjectProtocol {
    /**
     * obj 属性发生变化的对象。
     * property 发生变化的属性。
     */
    func observed<T>(object: T, changeAt property: String) where T: SPModel
}

// MARK: - Primary class
//@objc
open class SPModelBase: NSObject, SPModel {
    private
    var _onObservingMyselfProperties = false

    deinit {
        for (_, observer) in self.observers {
            for property in observer.observedKeies {
                self.removeObserver(self, forKeyPath: property, context: nil)
            }
        }
        
        guard _onObservingMyselfProperties else {
            return
        }
        
        var cls: AnyClass? = self.classForCoder
        repeat {
            var count: UInt32 = 0
            if let list = class_copyIvarList(cls, &count) {
                for idx in 0 ..< Int(count) {
                    let ivar = (list + idx).pointee
                    if let property = ivar_getName(ivar) {
                        let propertyName = String(cString: property)
                        self.removeObserver(self, forKeyPath: propertyName, context: nil)
                    }
                }
                list.deallocate()
            }
            cls = cls!.superclass()
        } while (cls != nil && cls != SPModelBase.self)
    }
    public override required
    init() {
        super.init()
    }
    
    /**
      * Initialize instance from a Dictionary instance.
     * As a subclass,You must override this method and call initWithDic: for super
     *  to ensure the instance variable _dictionary setted.
     */
    required public
    init(dic d: [String: Any]?) {
        super.init()
        guard let dic = d else {
            _dictionary = [:]
            return
        }
        
        _dictionary = dic.filterNullValues()
        
        var cls: SPModelBase.Type = Self.self
        
        repeat {
            /*
            var children: [(label: String, value: Any)]
            if cls == Self.self {
                children = Array(Mirror(reflecting: self).children) as! [(label: String, value: Any)]
            } else {
                children = Array(Mirror(reflecting: cls.init()).children) as! [(label: String, value: Any)]
            }
            */
            
            var count: UInt32 = 0
            if let list = class_copyIvarList(cls, &count) {
                for idx in 0 ..< Int(count) {
                    let ivar = (list + idx).pointee
                    
                    guard let property = ivar_getName(ivar) else {
                        continue
                    }
                    let name = String(cString: property)
                    self.addObserver(self, forKeyPath: name, options: .new, context: nil)
                    /*
                    for i in 0 ..< children.count {
                        let child = children[i]
                        
                        guard child.label == name else {
                            continue
                        }
                        children.remove(at: i)
                        
                        let string = String(describing: type(of: child.value))
                        switch string {
                        case "String", "Optional<String>":
                            var value = ""
                            if let v = dic[name] {
                                if let temp = v as? String {
                                    value = temp
                                } else {
                                    value = String(describing: v)
                                    if value.hasPrefix("Optional(") {
                                        value = value[9 ..< value.count]
                                    }
                                }
                            }
                            object_setIvar(self, ivar, value)
                            
//                            self.setValue(value, forKeyPath: name)
//                                let n = object_getIvar(self, ivar) as! String
                        case "Int", "Optional<Int>":
                            var value = 0
                            if let v = dic[name] {
                                if let temp = v as? Int {
                                    value = temp
                                } else if let temp = v as? Float {
                                    value = Int(temp)
                                } else if let temp = v as? Double {
                                    value = Int(temp)
                                } else {
                                    var str = String(describing: v)
                                    if str.hasPrefix("Optional(") {
                                        str = str[9 ..< str.count]
                                    }
                                    if let n = Int(str) {
                                        value = n
                                    }
                                }
                            }
                            object_setIvar(self, ivar, value)
                            
                        case "Double", "Optional<Double>":
                            var value = 0
                            if let v = dic[name] {
                                if let temp = v as? Int {
                                    value = temp
                                } else if let temp = v as? Float {
                                    value = Int(temp)
                                } else if let temp = v as? Double {
                                    value = Int(temp)
                                } else {
                                    var str = String(describing: v)
                                    if str.hasPrefix("Optional(") {
                                        str = str[9 ..< str.count]
                                    }
                                    if let n = Int(str) {
                                        value = n
                                    }
                                }
                            }
                            object_setIvar(self, ivar, value)
//                            Optional<Dictionary<String, Any>>
                        default:
                            var type = string
                            if type.hasPrefix("Optional<") {
                                type = type[9 ..< type.count]
                            }
                        }
                        break // for循环
                    }
                     */
                }
                list.deallocate()
            }
            
            cls = class_getSuperclass(cls) as! SPModelBase.Type
        } while (cls != SPModelBase.self)
    }

    /**
     * Create instances from an array which has included NSDictionary instances.
     */
    public static
    func from(dics: [[String : Any]]) -> [SPModel] {
        var array = [SPModel]()
        dics.filterNullValues().forEach { (d) in
            array.append(Self(dic: d))
        }
        return array
    }
    
    /**
     * Contains key-value-pair that's the dictionary for initialized.
     * And if the value changed after initialized, the value will change for the pair.
     */
    private
    var _dictionary: [String: Any] = [:]
    public
    var dictionary: [String: Any] {
        return _dictionary
    }
    
    public
    func toDictionary() -> [String: Any] {
        var dic = [String: Any]()
        var mir: Mirror! = Mirror(reflecting: self)
        repeat {
            let children = Array(mir.children) as! [(label: String, value: Any)]

            for child in children {
                dic[child.label] = child.value
            }
            
            mir = mir.superclassMirror
        } while (mir != nil && mir.subjectType != SPModelBase.self)
        
        return dic
    }

    /**
     Create instance without any argument.
     @return Instance.
     */
    public class
    func model() -> Self {
        return Self(dic: [:])
    }
//    private class
//    func emptyModel() -> Self {
//        return Self.init()
//    }
    private
    var observers: [String: ModelObserver] = [:]
    
    open override
    func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if object is SPModelBase , let propertyName = keyPath {
            
            var cls: SPModelBase.Type = Self.self
            nnnn: repeat {
                var children: [(label: String, value: Any)]
                if cls == Self.self {
                    children = Array(Mirror(reflecting: self).children) as! [(label: String, value: Any)]
                } else {
                    children = Array(Mirror(reflecting: cls.init()).children) as! [(label: String, value: Any)]
                }
                for child in children {
                    if child.label == propertyName {
                        _dictionary[propertyName] = child.value
                        break nnnn
                    }
                }
                cls = class_getSuperclass(cls) as! SPModelBase.Type
            } while (cls != SPModelBase.self)
        }
    }
}

class ModelObserver: NSObject {
    weak var delegate: ModelObserverDelegate?
    var observedKeies: Set<String> = []
    init(delegate: ModelObserverDelegate) {
        self.delegate = delegate
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if self.delegate != nil, let o = object as? SPModelBase , let p = keyPath {
            self.delegate?.observed(object: o, changeAt: p)
        }
    }
}


/// MARK: - SPModelBase扩展
// MARK: - Obseration
extension SPModelBase {
    
    public
    func observed<T: NSObject>(by obj: T, atProperty property: String) where T: ModelObserverDelegate {
        let key = obj.modelObserverKey
        
        
        var observer: ModelObserver! = self.observers[key]
        if observer == nil {
            observer = ModelObserver(delegate: obj)
            self.observers[key] = observer
        }
        
        if !observer.observedKeies.contains(property) {
            observer.observedKeies.insert(property)
            self.addObserver(observer, forKeyPath: property, options: .new, context: nil)
        }
    }
    public
    func observed<T: NSObject>(by obj: T, atProperties properties: [String]) where T: ModelObserverDelegate {
        let key = obj.modelObserverKey
        
        var observer: ModelObserver! = self.observers[key]
        if observer == nil {
            observer = ModelObserver(delegate: obj)
            self.observers[key] = observer
        }
        for property in properties {
            if !observer.observedKeies.contains(property) {
                observer.observedKeies.insert(property)
                self.addObserver(observer, forKeyPath: property, options: .new, context: nil)
            }
        }
    }
    public
    func removeObserved<T: NSObject>(by obj: T, atProperties properties: [String]) where T: ModelObserverDelegate {
        let key = obj.modelObserverKey
        guard let observer = self.observers[key] else {
            return
        }
        
        for p in properties {
            if observer.observedKeies.contains(p) {
                self.removeObserver(observer, forKeyPath: p, context: nil)
                observer.observedKeies.remove(p)
            }
        }
        if observer.observedKeies.isEmpty {
            self.observers.removeValue(forKey: key)
        }
    }
    public
    func removeObserved<T: NSObject>(by obj: T, atProperty property: String = "") where T: ModelObserverDelegate {
        let key = obj.modelObserverKey
        guard let observer = self.observers[key] else {
            return
        }
        if property.isEmpty {
            for p in observer.observedKeies {
                self.removeObserver(observer, forKeyPath: p, context: nil)
            }
            self.observers.removeValue(forKey: key)
        } else {
            for p in observer.observedKeies {
                self.removeObserver(observer, forKeyPath: p, context: nil)
            }
            if observer.observedKeies.isEmpty {
                self.observers.removeValue(forKey: key)
            }
        }
    }
}

extension NSObject {
    fileprivate
    var modelObserverKey: String {
        get {
            if let v = objc_getAssociatedObject(self, "modelObserverKey") as? String {
                return v
            }
            let key = String(format: "%p", self)
            self.modelObserverKey = key
            return key
        }
        set {
            objc_setAssociatedObject(self, "modelObserverKey", newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
    }
    
}

// MARK: - Persistence
extension SPModelBase {
    
    static
    var storedPath: String {
        return SPMODEL_SAVED_PATH + "/\(Self.self)/"
    }
    
    public
    func saveToDisk(_ forKey: String = "default") {
        let path = Self.storedPath + forKey + ".dat"
        NSDictionary(dictionary: _dictionary).write(toFile: path, atomically: true)
    }
    
    public
    func clearInDisk(_ forKey: String = "default") {
        let path = Self.storedPath + forKey + ".dat"
        do {
            try FileManager.default.removeItem(atPath: path)
        } catch { }
    }
    
    public
    func loadFromDisk(_ forKey: String = "default") -> Self {
        let path = Self.storedPath + forKey + ".dat"
        if let d = NSDictionary(contentsOfFile: path) {
            let a = Dictionary<String, Any>(_immutableCocoaDictionary: d)
            return Self(dic: a)
        }
        return Self.model()
    }
}

// MARK: - CustomDescription
extension SPModelBase {
    
    public override var description: String {
        var desc = ""
        var prefix = "{| "
//        var cls: SPModelBase.Type = Self.self
        var mir: Mirror! = Mirror(reflecting: self)
        repeat {
            let children = Array(mir.children) as! [(label: String, value: Any)]

            for child in children {
                if var v = child.value as? String {
                    if v == "" {
                        v = "<blank>"
                    }
                    desc += prefix + child.label + " -> " + v + "\n"
                } else {
                    var str = String(describing: child.value)
                    if str.hasPrefix("Optional(") {
                        str = str[9 ..< str.count]
                    }
                    if str == "nil" {
                        str = "<nil>"
                    }
                    if str.contains("["), str.contains("]") {
                        if str.contains("\": ") {
                            var dicStrs: [String] = []
                            for temp in str[1 ..< str.count].components(separatedBy: ",") {
                                if temp.contains("\n") {
                                    dicStrs.append(temp.replacingOccurrences(of: "\": ", with: "\":\n").replacingOccurrences(of: "\n", with: "\n  "))
                                } else {
                                    dicStrs.append(temp)
                                }
                            }
                            str = "[" + dicStrs.joined(separator: ",") + "]"
                        }
                        if str.contains(",") {
                            str = str.replacingOccurrences(of: ", ", with: ",\n")
                        }
                        if str.contains("\n") {
                            str = str.replacingOccurrences(of: "\n", with: "\n            ")
                        }
                    }else{
                        if str.contains("\n") {
                            str = str.replacingOccurrences(of: "\n", with: "\n           ")
                        }
                    }
                    
                    desc += prefix + child.label + " -> " + str + "\n"
                }
                if prefix != " | " {
                    prefix = " | "
                }
            }
            
            mir = mir.superclassMirror
        } while (mir != nil && mir.subjectType != SPModelBase.self)
        
        desc += String(format: "} <= <\(Self.self): %p>", self)
        return desc
    }
}

